import * as blocks from '@notes/blocks';
import { __unstableSchemas, AffineSchemas } from '@notes/blocks/models';
import * as editor from '@notes/editor';
import { EditorContainer } from '@notes/editor';
import {
    configDebugLog,
    disableDebuglog,
    enableDebugLog,
} from '@notes/global/debug';
import * as globalUtils from '@notes/global/utils';
import { assertExists } from '@notes/global/utils';
import type {
    BlobStorage,
    DocProviderCreator,
    Page,
    PassiveDocProvider,
    Y,
} from '@notes/store';
import * as store from '@notes/store';
import {
    createIndexeddbStorage,
    createMemoryStorage,
    createSimpleServerStorage,
    Generator,
    Schema,
    Utils,
    Workspace,
    type WorkspaceOptions,
} from '@notes/store';
import { createBroadcastChannelProvider } from '@notes/store/providers/broadcast-channel';
import type { IndexedDBProvider } from '@toeverything/y-indexeddb';
import { createIndexedDBProvider } from '@toeverything/y-indexeddb';
import { fileOpen } from 'browser-fs-access';

const params = new URLSearchParams(location.search);
const room = params.get('room') ?? Math.random().toString(16).slice(2, 8);
const providerArgs = (params.get('providers') ?? 'bc').split(',');
const blobStorageArgs = (params.get('blobStorage') ?? 'memory').split(',');
const featureArgs = (params.get('features') ?? '').split(',');

class IndexedDBProviderWrapper implements PassiveDocProvider {
    public readonly flavour = 'blocksuite-indexeddb';
    public readonly passive = true as const;
    private _connected = false;
    private _provider: IndexedDBProvider;
    constructor(doc: Y.Doc) {
        this._provider = createIndexedDBProvider(doc);
    }
    connect() {
        this._provider.connect();
        this._connected = true;
    }
    disconnect() {
        this._provider.disconnect();
        this._connected = false;
    }
    get connected() {
        return this._connected;
    }
}

export const defaultMode =
    params.get('mode') === 'edgeless' ? 'edgeless' : 'page';
export const initParam = params.get('init');
export const isE2E = room.startsWith('playwright');

export const getOptions = (
    fn: (params: URLSearchParams) => Record<string, string | number>
) => fn(params);

declare global {
    // eslint-disable-next-line no-var
    var targetPageId: string | undefined;
    // eslint-disable-next-line no-var
    var debugWorkspace: Workspace | undefined;
}

if (isE2E) {
    Object.defineProperty(window, '$blocksuite', {
        value: Object.freeze({
            store,
            blocks,
            global: { utils: globalUtils },
            editor,
        }),
    });
} else {
    Object.defineProperty(globalThis, 'openFromFile', {
        value: async function importFromFile(pageId?: string) {
            const file = await fileOpen({
                extensions: ['.ydoc'],
            });
            const buffer = await file.arrayBuffer();
            if (pageId) {
                globalThis.targetPageId = pageId;
            }
            Workspace.Y.applyUpdate(window.workspace.doc, new Uint8Array(buffer));
        },
    });

    Object.defineProperty(globalThis, 'rebuildPageTree', {
        value: async function rebuildPageTree(doc: Y.Doc, pages: string[]) {
            const pageTree = doc
                .getMap<Y.Array<Y.Map<unknown>>>('space:meta')
                .get('pages');
            if (pageTree) {
                const pageIds = pageTree.map(p => p.get('id') as string).filter(v => v);
                for (const page of pages) {
                    if (!pageIds.includes(page)) {
                        const map = new Workspace.Y.Map([
                            ['id', page],
                            ['title', ''],
                            ['createDate', +new Date()],
                            ['subpageIds', []],
                        ]);
                        pageTree.push([map]);
                    }
                }
            }
        },
    });

    Object.defineProperty(globalThis, 'debugFromFile', {
        value: async function debuggerFromFile() {
            const file = await fileOpen({
                extensions: ['.ydoc'],
            });
            const buffer = await file.arrayBuffer();
            const schema = new Schema();
            schema.register(AffineSchemas).register(__unstableSchemas);

            const workspace = new Workspace({
                schema,
                id: 'temporary',
            });
            Workspace.Y.applyUpdate(workspace.doc, new Uint8Array(buffer));
            globalThis.debugWorkspace = workspace;
        },
    });
}

export const isBase64 =
    /^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$/;

export function initDebugConfig() {
    Object.defineProperty(globalThis, 'enableDebugLog', {
        value: enableDebugLog,
    });
    Object.defineProperty(globalThis, 'disableDebugLog', {
        value: disableDebuglog,
    });
    Object.defineProperty(globalThis, 'configDebugLog', {
        value: configDebugLog,
    });

    // Uncomment this line or paste it into console to enable debug log.
    // enableDebugLog(['CRUD']);
}

async function initWithMarkdownContent(
    workspace: Workspace,
    url: URL,
    pageId: string
) {
    const { empty: emptyInit } = await import('./data/index.js');

    emptyInit(workspace, pageId);
    const page = workspace.getPage(pageId);
    assertExists(page);
    assertExists(page.root);
    const content = await fetch(url).then(res => res.text());
    const contentParser = new window.ContentParser(page);
    return contentParser.importMarkdown(content, page.root.id);
}

export async function tryInitExternalContent(
    workspace: Workspace,
    initParam: string,
    pageId: string
) {
    if (isValidUrl(initParam)) {
        const url = new URL(initParam);
        await initWithMarkdownContent(workspace, url, pageId);
    } else if (isBase64.test(initParam)) {
        Utils.applyYjsUpdateV2(workspace, initParam);
    }
}

/**
 * Provider configuration is specified by `?providers=broadcast` or `?providers=indexeddb,broadcast` in URL params.
 * We use BroadcastChannelProvider by default if the `providers` param is missing.
 */
export function createWorkspaceOptions(): WorkspaceOptions {
    const providerCreators: DocProviderCreator[] = [];
    const blobStorages: ((id: string) => BlobStorage)[] = [];
    const schema = new Schema();
    schema.register(AffineSchemas).register(__unstableSchemas);

    let idGenerator: Generator = Generator.AutoIncrement; // works only in single user mode

    if (providerArgs.includes('idb')) {
        providerCreators.push((_id, doc) => new IndexedDBProviderWrapper(doc));
        idGenerator = Generator.NanoID; // works in production
    }

    if (providerArgs.includes('bc')) {
        providerCreators.push(createBroadcastChannelProvider);
        idGenerator = Generator.NanoID; // works in production
    }

    if (blobStorageArgs.includes('memory')) {
        blobStorages.push(createMemoryStorage);
    }

    if (blobStorageArgs.includes('idb')) {
        blobStorages.push(createIndexeddbStorage);
    }

    if (blobStorageArgs.includes('mock')) {
        blobStorages.push(createSimpleServerStorage);
    }

    if (isE2E) {
        // We need a predictable id generator in single page test environment.
        // Keep in mind that with this config, the collaboration will easily crash,
        // because all clients will count id from 0.
        idGenerator = Generator.AutoIncrement;
    }

    return {
        id: room,
        schema,
        providerCreators,
        idGenerator,
        blobStorages,
        defaultFlags: {
            enable_toggle_block: featureArgs.includes('toggle'),
            enable_transformer_clipboard: featureArgs.includes('clipboard'),
            enable_set_remote_flag: true,
            enable_block_hub: true,
            enable_bookmark_operation: true,
            enable_note_index: true,
            enable_bultin_ledits: true,
            readonly: {
                'page:home': false,
            },
        },
    };
}

export function isValidUrl(urlLike: string) {
    let url;
    try {
        url = new URL(urlLike);
    } catch (_) {
        return false;
    }
    return url.protocol === 'http:' || url.protocol === 'https:';
}

export const createEditor = (page: Page, element: HTMLElement) => {
    const editor = new EditorContainer();
    editor.page = page;
    element.append(editor);

    editor.createBlockHub().then(blockHub => {
        document.body.appendChild(blockHub);
    });
    return editor;
};
